 
package com.mixide.ui.part;

import javax.inject.Inject;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.eclipse.e4.core.services.events.IEventBroker;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.TreeAdapter;
import org.eclipse.swt.events.TreeEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.TreeItem;

import com.mixide.ui.service.IconCache;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor(onConstructor_=@Inject)
public class ProjectPart {
	
	/* Tree view */
	private static final String TREEITEMDATA_FILE = "TreeItem.file";
		// File: File associated with tree item
	private static final String TREEITEMDATA_IMAGEEXPANDED = "TreeItem.imageExpanded";
		// Image: shown when item is expanded
	private static final String TREEITEMDATA_IMAGECOLLAPSED = "TreeItem.imageCollapsed";
		// Image: shown when item is collapsed
	private static final String TREEITEMDATA_STUB = "TreeItem.stub";
		// Object: if not present or null then the item has not been populated

	private TreeViewer tree;

	private final Display display;
	private final IEventBroker eventBroker;
	private final IconCache iconCache;
	
	@PostConstruct
	public void postConstruct(Composite parent) {
		this.iconCache.initResources(display);
		this.createTreeView(parent);
	}
	
	@PreDestroy
	public void preDispose() {
		this.tree.getTree().dispose();
	}
	
	/**
	 * Creates the file tree view.
	 *
	 * @param parent the parent control
	 */
	private void createTreeView(Composite parent) {
		Composite composite = new Composite(parent, SWT.NONE);
		GridLayout gridLayout = new GridLayout();
		gridLayout.numColumns = 1;
		composite.setLayout(gridLayout);

		tree = new TreeViewer(composite, SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL | SWT.SINGLE);
		tree.getTree().setLayoutData(new GridData(GridData.FILL_HORIZONTAL | GridData.FILL_VERTICAL));
		
		tree.getTree().addTreeListener(new TreeAdapter() {
			@Override
			public void treeExpanded(TreeEvent event) {
				final TreeItem item = (TreeItem) event.item;
				final Image image = (Image) item.getData(TREEITEMDATA_IMAGEEXPANDED);
				if (image != null) item.setImage(image);
				treeExpandItem(item);
			}
			@Override
			public void treeCollapsed(TreeEvent event) {
				final TreeItem item = (TreeItem) event.item;
				final Image image = (Image) item.getData(TREEITEMDATA_IMAGECOLLAPSED);
				if (image != null) item.setImage(image);
			}
		});
		tree.getTree().addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent event) {
				final TreeItem item = (TreeItem) event.item;
				File file = (File) item.getData(TREEITEMDATA_FILE);
				if (!file.isDirectory() && file.getName().endsWith(".mix")) {
					ProjectPart.this.eventBroker.post("MIX-FILE-SELECTED", file);
				}
			}
		});
		File[] roots = this.getRoots();
		this.treeRefresh(roots);
	}

	/**
	 * Handles expand events on a tree item.
	 *
	 * @param item the TreeItem to fill in
	 */
	private void treeExpandItem(TreeItem item) {
		final Object stub = item.getData(TREEITEMDATA_STUB);
		if (stub == null) treeRefreshItem(item, true);
	}

	/**
	 * Traverse the entire tree and update only what has changed.
	 *
	 * @param roots the root directory listing
	 */
	private void treeRefresh(File[] masterFiles) {
		TreeItem[] items = tree.getTree().getItems();
		int masterIndex = 0;
		int itemIndex = 0;
		for (int i = 0; i < items.length; ++i) {
			final TreeItem item = items[i];
			final File itemFile = (File) item.getData(TREEITEMDATA_FILE);
			if ((itemFile == null) || (masterIndex == masterFiles.length)) {
				// remove bad item or placeholder
				item.dispose();
				continue;
			}
			final File masterFile = masterFiles[masterIndex];
			int compare = compareFiles(masterFile, itemFile);
			if (compare == 0) {
				// same file, update it
				treeRefreshItem(item, false);
				++itemIndex;
				++masterIndex;
			} else if (compare < 0) {
				// should appear before file, insert it
				TreeItem newItem = new TreeItem(tree.getTree(), SWT.NONE, itemIndex);
				treeInitVolume(newItem, masterFile);
				if (masterFile.isDirectory()) {
					new TreeItem(newItem, SWT.NONE); // add a placeholder child item so we get the "expand" button
				}
				++itemIndex;
				++masterIndex;
				--i;
			} else {
				// should appear after file, delete stale item
				item.dispose();
			}
		}
		for (;masterIndex < masterFiles.length; ++masterIndex) {
			final File masterFile = masterFiles[masterIndex];
			TreeItem newItem = new TreeItem(tree.getTree(), SWT.NONE);
			treeInitVolume(newItem, masterFile);
			if (masterFile.isDirectory()) {
				new TreeItem(newItem, SWT.NONE); // add a placeholder child item so we get the "expand" button
			}
		}
	}

	/**
	 * Traverse an item in the tree and update only what has changed.
	 *
	 * @param dirItem the tree item of the directory
	 * @param forcePopulate true iff we should populate non-expanded items as well
	 */
	private void treeRefreshItem(TreeItem dirItem, boolean forcePopulate) {
		final File dir = (File) dirItem.getData(TREEITEMDATA_FILE);

		if (! forcePopulate && ! dirItem.getExpanded()) {
			// Refresh non-expanded item
			if (dirItem.getData(TREEITEMDATA_STUB) != null) {
				treeItemRemoveAll(dirItem);
				if (dir.isDirectory()) {
					new TreeItem(dirItem, SWT.NONE); // add a placeholder child item so we get the "expand" button
				}
				dirItem.setData(TREEITEMDATA_STUB, null);
			}
			return;
		}
		// Refresh expanded item
		dirItem.setData(TREEITEMDATA_STUB, this); // clear stub flag

		/* Get directory listing */
		File[] subFiles = (dir != null) ? FileViewer.getDirectoryList(dir) : null;
		if (subFiles == null || subFiles.length == 0) {
			/* Error or no contents */
			treeItemRemoveAll(dirItem);
			dirItem.setExpanded(false);
			return;
		}

		/* Refresh sub-items */
		TreeItem[] items = dirItem.getItems();
		final File[] masterFiles = subFiles;
		int masterIndex = 0;
		int itemIndex = 0;
		File masterFile = null;
		for (int i = 0; i < items.length; ++i) {
			while ((masterFile == null) && (masterIndex < masterFiles.length)) {
				masterFile = masterFiles[masterIndex++];
//				if (! masterFile.isDirectory()) masterFile = null;
			}

			final TreeItem item = items[i];
			final File itemFile = (File) item.getData(TREEITEMDATA_FILE);
			if ((itemFile == null) || (masterFile == null)) {
				// remove bad item or placeholder
				item.dispose();
				continue;
			}
			int compare = compareFiles(masterFile, itemFile);
			if (compare == 0) {
				// same file, update it
				treeRefreshItem(item, false);
				masterFile = null;
				++itemIndex;
			} else if (compare < 0) {
				// should appear before file, insert it
				TreeItem newItem = new TreeItem(dirItem, SWT.NONE, itemIndex);
				treeInitFolder(newItem, masterFile);
				if (masterFile.isDirectory()) {
					new TreeItem(newItem, SWT.NONE); // add a placeholder child item so we get the "expand" button
				}
				masterFile = null;
				++itemIndex;
				--i;
			} else {
				// should appear after file, delete stale item
				item.dispose();
			}
		}
		while ((masterFile != null) || (masterIndex < masterFiles.length)) {
			if (masterFile != null) {
				TreeItem newItem = new TreeItem(dirItem, SWT.NONE);
				treeInitFolder(newItem, masterFile);
				if (masterFile.isDirectory()) {
					new TreeItem(newItem, SWT.NONE); // add a placeholder child item so we get the "expand" button
				}
				if (masterIndex == masterFiles.length) break;
			}
			masterFile = masterFiles[masterIndex++];
//			if (! masterFile.isDirectory()) masterFile = null;
		}
	}

	/**
	 * Foreign method: removes all children of a TreeItem.
	 * @param treeItem the TreeItem
	 */
	private static void treeItemRemoveAll(TreeItem treeItem) {
		final TreeItem[] children = treeItem.getItems();
		for (TreeItem child : children) {
			child.dispose();
		}
	}

	/**
	 * Initializes a folder item.
	 *
	 * @param item the TreeItem to initialize
	 * @param folder the File associated with this TreeItem
	 */
	private void treeInitFolder(TreeItem item, File folder) {
		item.setText(folder.getName());
		if (folder.isDirectory()) {
			item.setImage(iconCache.stockImages[iconCache.iconClosedFolder]);
		} else {
			if (folder.getName().endsWith(".mix")) {
				item.setImage(iconCache.stockImages[iconCache.iconMix]);
				item.addListener(SWT.Selection, event -> {
					System.out.println(event);
				});
			} else {
				item.setImage(iconCache.stockImages[iconCache.iconFile]);
			}
		}
		item.setData(TREEITEMDATA_FILE, folder);
		item.setData(TREEITEMDATA_IMAGEEXPANDED, iconCache.stockImages[iconCache.iconOpenFolder]);
		item.setData(TREEITEMDATA_IMAGECOLLAPSED, iconCache.stockImages[iconCache.iconClosedFolder]);
	}

	/**
	 * Initializes a volume item.
	 *
	 * @param item the TreeItem to initialize
	 * @param volume the File associated with this TreeItem
	 */
	private void treeInitVolume(TreeItem item, File volume) {
		item.setText(volume.getPath());
		item.setImage(iconCache.stockImages[iconCache.iconClosedDrive]);
		item.setData(TREEITEMDATA_FILE, volume);
		item.setData(TREEITEMDATA_IMAGEEXPANDED, iconCache.stockImages[iconCache.iconOpenDrive]);
		item.setData(TREEITEMDATA_IMAGECOLLAPSED, iconCache.stockImages[iconCache.iconClosedDrive]);
	}
	
	/**
	 * Gets filesystem root entries
	 *
	 * @return an array of Files corresponding to the root directories on the platform,
	 *         may be empty but not null
	 */
	File[] getRoots() {
		/*
		 * On JDK 1.22 only...
		 */
		// return File.listRoots();

		/*
		 * On JDK 1.1.7 and beyond...
		 * -- PORTABILITY ISSUES HERE --
		 */
		if (System.getProperty ("os.name").contains ("Windows")) {
			List<File> list = new ArrayList<>();
			for (char i = 'c'; i <= 'z'; ++i) {
				File drive = new File(i + ":" + File.separator);
				if (drive.isDirectory() && drive.exists()) {
					list.add(drive);
				}
			}
			File[] roots = list.toArray(new File[list.size()]);
			sortFiles(roots);
			return roots;
		}
		File root = new File(File.separator);
		return new File[] { root };
	}

	/**
	 * Gets a directory listing
	 *
	 * @param file the directory to be listed
	 * @return an array of files this directory contains, may be empty but not null
	 */
	static File[] getDirectoryList(File file) {
		File[] list = file.listFiles();
		if (list == null) return new File[0];
		sortFiles(list);
		return list;
	}

	/**
	 * Sorts files lexicographically by name.
	 *
	 * @param files the array of Files to be sorted
	 */
	static void sortFiles(File[] files) {
		/* Very lazy merge sort algorithm */
		sortBlock(files, 0, files.length - 1, new File[files.length]);
	}
	private static void sortBlock(File[] files, int start, int end, File[] mergeTemp) {
		final int length = end - start + 1;
		if (length < 8) {
			for (int i = end; i > start; --i) {
				for (int j = end; j > start; --j)  {
					if (compareFiles(files[j - 1], files[j]) > 0) {
						final File temp = files[j];
						files[j] = files[j-1];
						files[j-1] = temp;
					}
				}
			}
			return;
		}
		final int mid = (start + end) / 2;
		sortBlock(files, start, mid, mergeTemp);
		sortBlock(files, mid + 1, end, mergeTemp);
		int x = start;
		int y = mid + 1;
		for (int i = 0; i < length; ++i) {
			if ((x > mid) || ((y <= end) && compareFiles(files[x], files[y]) > 0)) {
				mergeTemp[i] = files[y++];
			} else {
				mergeTemp[i] = files[x++];
			}
		}
		for (int i = 0; i < length; ++i) files[i + start] = mergeTemp[i];
	}
	
	private static int compareFiles(File a, File b) {
		int compare = a.getName().compareToIgnoreCase(b.getName());
		if (compare == 0) compare = a.getName().compareTo(b.getName());
		return compare;
	}
}